<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 1.什么是构造函数
      // 定义:构造函数就是你用new关键字创建对象时调用的函数
      // 作用(优点):创建多个共享特定属性和行为的对象,主要是用于生成对象的饼干模具
      // 缺点:当实例化多个对象时,会重复的创建对象,造成内存空间的浪费,增大CPU的开销，并没有消除代码的冗余,(如后面代码所示,原型正好解决了此类问题)
      // 声明一构造函数,首字母大写
      function Animal(name, age) {
        // this == new Animal();new会自动的创建this对象,且类型就是该构造函数的类型,构造函数不需要返回值,因为new会显示的返回,return的值就等于函数名+()的调用
        this.name = name // 自定义属性
        this.age = age // 同上
        this.fun = function () {
          // 自定义方法
          return this.name + ' ' + this.age + '岁了'
        }
      }
      // 当一个函数创建好以后，我们并不知道它是不是构造函数，即使像上面的例子一样，函数名为大写，我们也不能确定。
      // 只有当一个函数以 new 关键字来调用的时候，我们才能说它是一个构造函数。就像下面这样：
      // 实例化对象
      var animal1 = new Animal('cat', 2)
      // 过程：♥♥♥♥♥
      // 1.当以 new 关键字调用时，会创建一个新的内存空间，标记为 Animal 的实例animal1。
      // 2.函数体内部的 this 指向该内存。
      // 3.执行函数体内的代码 ：给 this 添加属性，就相当于给实例添加属性。
      // 4.默认返回 this
      // 由于函数体内部的 this 指向新创建的内存空间，默认返回 this ，就相当于默认返回了该内存空间，也就是上图中的 #f1。
      // 此时，#f1的内存空间被变量 p1 所接受。也就是说 p1 这个变量，保存的内存地址就是 #f1，同时被标记为 Person 的实例。
      var animal2 = new Animal('dog', 3)

      // 2.es6语法class 定义类 和原始构造函数类似
      // 构造函数的prototype属性，在 ES6 的“类”上面继续存在。事实上，类的所有方法都定义在类的prototype属性上面。 区别是class类内部定义的方法，它是不可枚举的！！
      class Point {
        constructor() {
          // ...
        }

        toString() {
          // ...
        }

        toValue() {
          // ...
        }
      }
      // 等同于
      Point.prototype = {
        constructor() {},
        toString() {},
        toValue() {},
      }

      // 1）constructor 方法：
      // constructor()方法是类的默认方法，通过new命令生成对象实例时，自动调用该方法。一个类必须有constructor()方法，如果没有显式定义，一个空的constructor()方法会被默认添加。

      // 2）取值函数（getter）和存值函数（setter）:与 ES5 一样，在“类”的内部可以使用get和set关键字，对某个属性设置存值函数和取值函数，拦截该属性的存取行为。
      class MyClass {
        constructor() {}
        get prop() {
          return '我直接返回的'
        }
        set prop(value) {
          console.log('setter: ' + value)
        }
      }

      let inst = new MyClass()
      // set设置值
      inst.prop = 123
      // get只读
      inst.prop //我直接返回的
      // console.log(inst.prop, ' inst.prop')

      //3）属性表达式：类的属性名，可以采用表达式
      let methodName = 'getArea'

      class Square {
        constructor(length) {}
        [methodName]() {
          console.log('hahaha')
        }
      }
      let p1 = new Square()
      p1.getArea() //hahaha

      // 4)Class 表达式 :与函数一样，类也可以使用表达式的形式定义。
      const MyClass1 = class Me {
        constructor(e) {
          this.name = e
        }
        getClassName() {
          return Me.name
        }
      }
      let a1 = new MyClass1('小明')
      a1.name //小明
      a1.getClassName() // Me
      // 上面代码表示，Me只在 Class 内部有定义。
      // 如果类的内部没用到的话，可以省略Me，也就是可以写成下面的形式。 const MyClass1 = class { /* ... */ };

      // !!!!!注意点:
      // 1.严格模式:类和模块的内部，默认就是严格模式，所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中，就只有严格模式可用。
      //           考虑到未来所有的代码，其实都是运行在模块之中，所以 ES6 实际上把整个语言升级到了严格模式。
      // 2.不存在提升:类不存在变量提升（hoist），这一点与 ES5 完全不同。
      // 3.name 属性:由于本质上，ES6 的类只是 ES5 的构造函数的一层包装，所以函数的许多特性都被Class继承，包括name属性。
      class Point1 {}
      Point1.name // "Point"  name属性总是返回紧跟在class关键字后面的类名。
      // 4.Generator 方法:如果某个方法之前加上星号（*），就表示该方法是一个 Generator 函数。
      class Yoo {
        constructor(...args) {
          this.args = args
        }
        *[Symbol.iterator]() {
          for (let arg of this.args) {
            yield arg
          }
        }
      }

      // for (let x of new Foo('hello', 'world')) {
      //   console.log(x)
      // }
      // hello
      // world
      //5.this 的指向 :类的方法内部如果含有this，它默认指向类的实例。但是，必须非常小心，一旦单独使用该方法，很可能报错。
      class Logger {
        printName(name = 'there') {
          this.print(`Hello ${name}`)
        }

        print(text) {
          console.log(text)
        }
      }

      const logger = new Logger()
      // const { printName } = logger
      // printName() // TypeError: Cannot read property 'print' of undefined
      // 上面代码中，printName方法中的this，默认指向Logger类的实例。但是，如果将这个方法提取出来单独使用，this会指向该方法运行时所在的环境（由于 class 内部是严格模式，所以 this 实际指向的是undefined），从而导致找不到print方法而报错。
      // 解决办法：(改变this的指向问题)
      class Logger1 {
        constructor() {
          this.printName = this.printName.bind(this)
          // 一个比较简单的解决方法是，在构造方法中绑定this，这样就不会找不到print方法了。
        }
        // ...
      }
      class Obj {
        constructor() {
          // 另一种解决方法是使用箭头函数。
          this.getThis = () => this
        }
      }

      const myObj = new Obj()
      myObj.getThis() === myObj // true

      // **************************************
      // 静态方法：类相当于实例的原型，所有在类中定义的方法，都会被实例继承。如果在一个方法前，加上static关键字，就表示该方法不会被实例继承，而是直接通过类来调用，这就称为“静态方法”。
      // class daoge {
      //   static classMethod() {
      //     console.log('hellow,i an daoge')
      //   }
      // }
      // var foo = new daoge()
      // foo.classMethod() // 带有static 时候会报错，因为没有被实例继承

      // **************************************
      // 实例属性的新写法：实例属性除了定义在constructor()方法里面的this上面，也可以定义在类的最顶层。
      class IncreasingCounter {
        _count = 0
        // constructor() {
        //   this._count = 0
        // }
        get value() {
          console.log('Getting the current value!')
          return this._count
        }
        increment() {
          this._count++
        }
      }
      // **************************************
      // 静态属性：和静态方法类似，是在类中的一个属性。因为 ES6 明确规定，Class 内部只有静态方法，没有静态属性。现在有一个提案提供了类的静态属性，写法是在实例属性的前面，加上static关键字。
      class daoge {
        static myStaticProp = 42
        constructor() {
          console.log(daoge.myStaticProp) // 42
        }
      }
      var foo = new daoge()
      // console.log(foo.myStaticProp, '  foo.myStaticProp')  //static属性后面的属性在事例中是undefined

      // **************************************
      // 私有方法和私有属性：

      // 现有的解决方案--》利用Symbol值的唯一性，将私有方法的名字命名为一个Symbol值。（Reflect.ownKeys()依然可以拿到它们。）
      // const bar = Symbol('bar')
      // const snaf = Symbol('snaf')

      // export default class myClass {
      //   // 公有方法
      //   foo(baz) {
      //     this[bar](baz)
      //   }
      //   // 私有方法
      //   [bar](baz) {
      //     return (this[snaf] = baz)
      //   }

      //   // ...
      // }
      // 私有属性的提案：目前，有一个提案，为class加了私有属性。方法是在属性名之前，使用#表示。
      class IncreasingCounter2 {
        #count = 0
        get value() {
          console.log('Getting the current value!')
          return this.#count
        }
        increment() {
          this.#count++
          console.log(this.#count, 'count')
        }
      }
      const counter = new IncreasingCounter2()
      // counter.increment() //1
      // counter.#count // 报错
      // counter.#count = 42 // 报错
      class Point222 {
        #x
        constructor(x = 0) {
          this.#x = +x
        }
        get x() {
          return this.#x
        }
        set x(value) {
          this.#x = +value
        }
      }
      // 上面代码中，#x就是私有属性，在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分，使用时必须带有#一起使用，所以#x和x是两个不同的属性。
      // 之所以要引入一个新的前缀#表示私有属性，而没有采用private关键字，是因为 JavaScript 是一门动态语言，没有类型声明，
      // 使用独立的符号似乎是唯一的比较方便可靠的方法，能够准确地区分一种属性是否为私有属性。另外，Ruby 语言使用@表示私有属性，ES6 没有用这个符号而使用#，是因为@已经被留给了 Decorator。
      // 这种写法不仅可以写私有属性，还可以用来写私有方法。

      // in 运算符:
      // class A {
      //   #foo = 0
      //   use(obj) {
      //     console.log(obj, 'obj')
      //     if (#foo in obj) {
      //       // 私有属性 #foo 存在
      //       alert('存在')
      //     } else {
      //       alert('不存在')
      //       // 私有属性 #foo 不存在
      //     }
      //   }
      // }
      // let aad = new A()
      // aad.use(new A()) //存在

      // **************************************
      // **************************************
      // new.target 属性：
      // new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性，该属性一般用在构造函数之中，返回new命令作用于的那个构造函数。
      // 如果构造函数不是通过new命令或Reflect.construct()调用的，new.target会返回undefined，因此这个属性可以用来确定构造函数是怎么调用的。
      function Person(name) {
        if (new.target !== undefined) {
          this.name = name
        } else {
          throw new Error('必须使用 new 命令生成实例')
        }
      }

      // 另一种写法
      function Person(name) {
        if (new.target === Person) {
          this.name = name
        } else {
          throw new Error('必须使用 new 命令生成实例')
        }
      }

      var person = new Person('张三') // 正确
      var notAPerson = Person.call(person, '张三') // 报错
    </script>
  </body>
</html>
